/*
 *  scoreBoard - Settings.cpp
 *  Copyright 2011 W. Reckman. All rights reserved.
 */
#include <iostream>
#include <fstream>
#include "ofConstants.h"
#include "ofUtils.h"
#include "Settings.h"

const char* Settings::WHITESPACE_CHARS = " \n\r\t\v";
const char* Settings::VALID_KEY_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
const char* Settings::COMMENT_MARKER = "#";


//--------------------------------------------------------------
Settings& Settings::getInstance() {
	if ( ! instance ) instance = new Settings();
	return *instance;
}

//--------------------------------------------------------------
/*
 * FIXME: nothing prevents duplicate keys from being stored, but except for the
 *        first one, none can be retrieved
 *
 * File format:
 * - Configuration items consist of one line: '<key> = <value>', everything
 *   after the first '=' is regarded as <value>, all whitespace will be trimmed.
 * - Lines starting with '[' denote a group: '[<groupname>]', configuration
 *   items before the first group marker will be in a nameless group which can
 *   subsequently be reselected with '[]'.
 * - Lines starting with '#' and those without any '=' will be ignored.
 */
bool Settings::loadFromFile( const string& filename, bool clearPrevious ) {
	std::ifstream ifs( ofToDataPath( filename ).c_str() );
	
	if ( ifs.is_open() ) {
		string line, currentGroup = "";
		int ln = 0;
		bool rv;
		while( ifs ) {
			getline( ifs, line );
			ln++;
			
			if ( ifs.fail() && ! ifs.eof() ) {
				ofLog( OF_LOG_ERROR, "(Settings.cpp) error reading from settings file '%s'", filename.c_str() );
				break;
			}
			
			rv = parseLine( line, currentGroup, filename, ln );
		}
		
	} else {
		ofLog( OF_LOG_ERROR, "(Settings.cpp) error opening settings file '%s'", filename.c_str() );
	}
	
	ifs.close();
}

//--------------------------------------------------------------
//Returns the value for the requested key, or NULL if such key does not exist.
const string* Settings::getValue( const string& key ) const {
	return getValueInternal( key, true, "" );
}

//--------------------------------------------------------------
//Returns the value for the requested key, or defaultValue if such key does not exist.
const string* Settings::getValue( const string& key, const string& defaultValue ) const {
	const string* rv = getValueInternal( key, true, "" );
	return rv ? rv : &defaultValue;
}

//--------------------------------------------------------------
//Returns the value for the requested key, or NULL if such key does not exist.
const string* Settings::getValueInGroup( const string& key, const string& group ) const {
	return getValueInternal( key, false, group );
}

//--------------------------------------------------------------
//Returns the value for the requested key, or defaultValue if such key does not exist.
const string* Settings::getValueInGroup( const string& key, const string& group, const string& defaultValue ) const {
	const string* rv = getValueInternal( key, false, group );
	return rv ? rv : &defaultValue;
}


/*********************
 * PRIVATE FUNCTIONS *
 *********************/

Settings* Settings::instance = 0;

//--------------------------------------------------------------
Settings::Settings()
: modified( false )
{}

//--------------------------------------------------------------
Settings::~Settings() {}


//--------------------------------------------------------------
const string* Settings::getValueInternal( const string& key, bool anyGroup, const string& group ) const {
	vec_ConfigItem::const_iterator it;
	const ConfigItem* result = 0;
	
	for ( it = items.begin(); it != items.end(); ++it ) {
		const ConfigItem& item = *it;
		if ( anyGroup || item.group == group ) {
			if ( item.key == key ) {
				result = &item;
				break;
			}
		}
	}
	
	return result ? &result->value : 0;
}

//--------------------------------------------------------------
//Returns true if a key/value pair has been added to the vector
bool Settings::parseLine( const string& line, string& group, const string& filename, int lineNumber ) {
	int firstCharPos = line.find_first_not_of( WHITESPACE_CHARS );
	int assignmentCharPos = line.find( '=' );
	int leftBracketPos = line.find( '[' );
	
	/* easy rejections */
	if ( line.length() == 0 ) return false;
	if ( line.find( COMMENT_MARKER ) == firstCharPos ) return false;
	if ( assignmentCharPos == string::npos && leftBracketPos == string::npos ) {
		if ( firstCharPos != string::npos ) {
			ofLog( OF_LOG_WARNING, "(Settings.cpp) ignored improperly formatted line (%s:%i)", filename.c_str(), lineNumber );
		}
		return false;
	}
	
	if ( leftBracketPos == string::npos ) {
		string key = line.substr( firstCharPos, assignmentCharPos - firstCharPos );
		string value = line.substr( assignmentCharPos + 1 );
		
		trim( key );
		trim( value );
		
		if ( key.find_first_not_of( VALID_KEY_CHARS ) != string::npos ) {
			ofLog( OF_LOG_WARNING, "(Settings.cpp) ignored key/value pair due to invalid characters (%s:%i)", filename.c_str(), lineNumber );
			return false;
		}
		
		items.push_back( ConfigItem( key, value, group ) );
		
		if ( group == "" ) {
			ofLog( OF_LOG_NOTICE, "(Settings.cpp) added config key '%s' with value '%s' (no group) from %s:%i", key.c_str(), value.c_str(), filename.c_str(), lineNumber );
		} else {
			ofLog( OF_LOG_NOTICE, "(Settings.cpp) added config key '%s' with value '%s' (group '%s') from %s:%i", key.c_str(), value.c_str(), group.c_str(), filename.c_str(), lineNumber );
		}
	} else {
		int rightBracketPos = line.rfind( ']' );
		if ( rightBracketPos != string::npos ) {
			string newGroup = line.substr( leftBracketPos + 1, rightBracketPos - leftBracketPos - 1 );
			group = newGroup;
		} else {
			ofLog( OF_LOG_WARNING, "(Settings.cpp) ignored what looked like an improperly formatted group name (%s:%i)", filename.c_str(), lineNumber );
		}
	}
	
	return true;
}

//--------------------------------------------------------------
void Settings::trim( string& s, bool trimLeading, bool trimTrailing ) {
	int first = 0, last = s.length();
	
	first = s.find_first_not_of( WHITESPACE_CHARS );
	last = s.find_last_not_of( WHITESPACE_CHARS );
	
	s = ( first != string::npos ) ? s.substr( first, last - first + 1 ) : "";
}
